package bigoo.ds;

import static bigoo.util.Config.DS_LEASE_TIME_PROP;
import static bigoo.util.Config.DS_THRESHOLD_PROP;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Logger;

import bigoo.common.PeerEntry;
import bigoo.util.Config;

/**
 * @author Danilo Penna Queiroz <dpenna.queiroz@gmail.com>
 * 
 */
public class PeerDirectory {

	Logger logger = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);
	private SortedSet<PeerEntry> peersSet = new TreeSet<PeerEntry>();
	private Map<String, PeerEntry> peersMap = new HashMap<String, PeerEntry>();
	private ScheduledExecutorService cleanerTimer = Executors.newSingleThreadScheduledExecutor();
	private Lock monitor = new ReentrantLock();
	private Properties props = Config.getProperties();

	public PeerDirectory() {
		this.cleanerTimer.scheduleAtFixedRate(new PeersRemover(), 0, Long.parseLong(props.getProperty(DS_LEASE_TIME_PROP))/2, TimeUnit.MILLISECONDS);
	}

	/**
	 * Inserts or updates, if peer is already inserted, a peer at peer
	 * directory.
	 * 
	 * @param peerID
	 * @param address
	 * @param port
	 */
	public int insertOrUpdatePeer(String peerID, String address, int port) {
		int leaseTime = Integer.parseInt(props.getProperty(DS_LEASE_TIME_PROP));
		this.monitor.lock();
		PeerEntry entry = getPeerEntry(peerID);
		entry.setAddress(address);
		entry.setPort(port);
		entry.setLeaseTime(this.getCurrentTime() + leaseTime);
		this.peersSet.add(entry);
		this.monitor.unlock();
		return leaseTime;
	}

	/**
	 * @param requestorID
	 * @return a collection of random online peers.
	 */
	public Collection<PeerEntry> getSomePeers(String requestorID) {
		this.monitor.lock();
		int threshold = getThreshold();
		Collection<PeerEntry> result = new ArrayList<PeerEntry>(threshold);
		Iterator<PeerEntry> itr = this.peersSet.iterator();
		for (int i = 0; i < threshold; i++) {
			PeerEntry peer = itr.next();
			if (!peer.getPeerID().equals(requestorID)) {
				result.add(peer);
			}
		}
		this.monitor.unlock();
		return result;
	}

	public PeerEntry getPeer(String peerID) {
		this.monitor.lock();
		try {
			return this.peersMap.get(peerID);
		} finally {
			this.monitor.unlock();
		}
	}

	public int getThreshold() {
		return Math.min(this.peersSet.size(), Integer.parseInt(props.getProperty(DS_THRESHOLD_PROP)));
	}

	/**
	 * Remove all 'old' peers, it means, all peers which lease has expired.
	 */
	private void removeOldPeers() {
		this.monitor.lock();
		if (!this.peersSet.isEmpty()) {
			PeerEntry refEntry = new PeerEntry();
			refEntry.setLeaseTime(this.getCurrentTime());

			SortedSet<PeerEntry> oldPeers = this.peersSet.headSet(refEntry);
			for (PeerEntry peer : oldPeers) {
				oldPeers.remove(peer);
				this.peersMap.remove(peer.getPeerID());
			}
		}
		logger.fine("Peers remaining: " + this.peersSet.size());
		this.monitor.unlock();
	}

	/**
	 * Gets a PeerEntry for the given peerID
	 */
	private PeerEntry getPeerEntry(String peerID) {
		PeerEntry entry;
		if (this.peersMap.containsKey(peerID)) {
			entry = this.peersMap.get(peerID);
			this.peersSet.remove(entry);
		} else {
			entry = new PeerEntry(peerID);
			this.peersMap.put(peerID, entry);
		}
		return entry;
	}

	protected long getCurrentTime() {
		return System.currentTimeMillis();
	}

	/**
	 * This class is responsible by, periodically, remove peers which lease has
	 * expired.
	 */
	private class PeersRemover implements Runnable {

		@Override
		public void run() {
			logger.info("Checking peer to be removed!");
			removeOldPeers();
		}

	}

}
